/*
 * Copyright (c) 2007, 2008 Ulrich Hilger, http://dev.uhilger.de, all rights reserved.
 *
 * Published under the terms and conditions of the Apache License 2.0,
 * see http://www.apache.org/licenses/LICENSE-2.0
 */

package de.uhilger.lib.filesystem;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Vector;
import java.util.zip.Adler32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * This class implements interface <code>FileSystem</code> for the file system of the 
 * machine this class is instantiated on. The implementation relies on the standard 
 * Java class <code>java.io.File</code> and wraps its methods into the methods required by 
 * interface <code>FileSystem</code> as required and where possible. In addition it implements 
 * methods not supported by class java.io.File such as copy, move, etc. 
 * 
 * @author Ulrich Hilger, http://dev.uhilger.de
 * @author Published under the terms and conditions of
 * the <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank">Apache License 2.0</a>
 *
 * @version 3, 4.3.2008
 * @see FileSystem
 * @see java.io.File
 */
public class LocalFileSystem implements FileSystem {

	/** the listeners to notify of changes */
	private Vector listeners = new Vector(0);

	/** indicator whether or not a copy thread currently is in progress */
	private boolean copyInProgress = false;

	/**
	 * create a new instance of class <code>LocalFileSystem</code>
	 */
	public LocalFileSystem() {
		super();
	}

	/**
	 * get the files that are contained in a given folder
	 * 
	 * @param folder  the directory to get the files for
	 * @return  the list of files contained in the given directory
	 * @throws Exception
	 */
	public FileRef[] list(FileRef folder) {
		File file = new File(folder.getAbsolutePath());
		File[] files = file.listFiles();
		FileRef[] gfiles = null;
		if(files != null && files.length > 0) {
			gfiles = new FileRef[files.length];
			for(int i = 0; i < files.length; i++) {
				gfiles[i] = localToGenericFile(files[i]);
			}
		} else {
			gfiles = new FileRef[0];
		}
		return gfiles;
	}

	/**
	 * get the file that is directly on top of a given file in 
	 * the file hierarchy of this file system
	 * 
	 * @param file  the file to get the parent file for
	 * @return  the parent file
	 * @throws Exception
	 */
	public FileRef getParentFile(FileRef file) {
		File localFile = new File(file.getAbsolutePath());
		File parent = localFile.getParentFile();
		return localToGenericFile(parent);
	}

	/**
	 * get the root directories of this file system
	 * 
	 * @return  the root directories of this file system
	 * @throws Exception
	 */
	public FileRef[] getRoots() {
		File[] roots = File.listRoots();
		FileRef[] groots = new FileRef[roots.length];
		for(int i = 0; i < roots.length; i++) {
			groots[i] = localToGenericFile(roots[i]);
		}
		return groots;
	}

	/**
	 * get the string that is used to separate file and folder names in path expressions 
	 * for the machine this file system refers to
	 * 
	 * @return the separator string
	 * @throws Exception
	 */
	public String getFileSeparator() {
		return File.separator;
	}	

	/**
	 * copy one file to another
	 * @param fromFile  the file to copy
	 * @param toFile the file to copy to (will be overwritten, when exists)
	 */
	private boolean copy(FileRef fromFile, FileRef toFile) throws Exception {
		boolean fireEvents = (listeners.size() > 0);
		File from = new File(fromFile.getAbsolutePath());
		File to = new File(toFile.getAbsolutePath());
		to.getParentFile().mkdirs();
		if(!to.exists())
			to.createNewFile();
		byte[] bytes = new byte[1024*50];		
		FileInputStream in = new FileInputStream(from);
		FileOutputStream out = new FileOutputStream(to);
		int bytesRead = in.read(bytes);
		while(bytesRead != -1) { 
			out.write(bytes, 0, bytesRead);
			if(fireEvents) {
				fireContentChanged(
						new FileSystemEvent(this, FileSystemEvent.ACTION_COPY, FileSystemEvent.PROGRESS, new FileRef[] {fromFile}, 
								(double) bytesRead));
			}
			bytesRead = in.read(bytes);
		}
		out.flush();
		in.close();
		out.close();
		return true;
	}

	/**
	 * copy a group of files to a folder inside this file system. This method returns 
	 * immediately and performs the copy process in a separate thread. Objects that like to 
	 * track the copy process need to implement interface FileSystemListener. Only one 
	 * copy thread can be started at a time.
	 * 
	 * @param filesToCopy  the files to copy
	 * @param toFolder the folder to copy to
	 * @return true, after the copy thread has been started
	 */
	public boolean copy(FileRef[] filesToCopy, FileRef toFolder) throws Exception {
		CopyThread t = new CopyThread(this, filesToCopy, toFolder);
		t.start();
		return true;
	}

	/**
	 * A thread to copy a group of files to a given folder inside a given file system.
	 * 
	 * @author Ulrich Hilger
	 * @author Light Development
	 * @author <a href="http://www.lightdev.com">http://www.lightdev.com</a>
	 * @author <a href="mailto:info@lightdev.com">info@lightdev.com</a>
	 * @author published under the terms and conditions of the BSD License,
	 *      for details see file license.txt in the distribution package of this software
	 *
	 * @version 1, 20.3.2008
	 * @see com.lightdev.lib.filesystem.LocalFileSystem
	 */
	public class CopyThread extends Thread {

		private FileRef[] filesToCopy;
		private FileRef toFolder;
		private FileSystem fileSystem;

		/**
		 * Create a new instance of class CopyThread
		 * @param fileSystem
		 * @param filesToCopy
		 * @param toFolder
		 */
		public CopyThread(FileSystem fileSystem, FileRef[] filesToCopy, FileRef toFolder) {
			super();
			this.filesToCopy = filesToCopy;
			this.toFolder = toFolder;
			this.fileSystem = fileSystem;
		}

		/**
		 * run this thread
		 */
		public void run()  {
			boolean fireEvents = (listeners.size() > 0);
			if(!copyInProgress) {
				try {
					copyInProgress = true;
					if(fireEvents) {
						double size = getSize(filesToCopy);
						fireContentChanged(new FileSystemEvent(fileSystem, FileSystemEvent.ACTION_COPY, 
								FileSystemEvent.PROCESS_START, filesToCopy, size));
					}
					copyRecursive(filesToCopy, toFolder); 
					copyInProgress = false;
					if(fireEvents) {
						fireContentChanged(new FileSystemEvent(fileSystem, FileSystemEvent.ACTION_COPY, 
								FileSystemEvent.PROCESS_END, filesToCopy));
					}
				} 
				catch (Throwable e) { 
					copyInProgress = false;
					if(fireEvents) {
						fireContentChanged(new FileSystemEvent(fileSystem, FileSystemEvent.ACTION_COPY, 
								FileSystemEvent.PROCESS_END, filesToCopy));
					}
					e.printStackTrace(); 
				}
			}
		}
	}

	/**
	 * get the total size of a number of files
	 * @param files  the files to determine the total size for
	 * @return  the number of byte the given files have in total
	 * @throws Throwable
	 */
	public double getSize(FileRef[] files) throws Exception {
		double size = 0;
		for(int i = 0; i < files.length; i++) {
			if(files[i].isDirectory()) {
				size += getFolderSize(files[i]);
			} else {
				size += files[i].getLength();
			}
		}
		return size;
	}

	/**
	 * copy files to a given folder. Overwrites existing files with the same 
	 * name inside the target folder.
	 * 
	 * @param filesToCopy  the files to copy
	 * @param toFolder  the directory to copy to
	 * @throws Exception
	 */
	private boolean copyRecursive(FileRef[] filesToCopy, FileRef toFolder) throws Exception {
		for(int i = 0; i < filesToCopy.length; i++) {
			if(filesToCopy[i].isDirectory())
				copyRecursive(list(filesToCopy[i]), mix(filesToCopy[i], toFolder));
			else
				copy(filesToCopy[i], mix(filesToCopy[i], toFolder));
		}
		return true;
	}

	/**
	 * rename a file, i.e. change the name of a file but not its location
	 * 
	 * @param file  the file to rename
	 * @param newName  the new name; just the name, no path
	 * @throws Exception
	 */
	public FileRef rename(FileRef file, String newName) throws Exception {
		File fileToRename = new File(file.getAbsolutePath());
		String newAbsolutePath = 
			fileToRename.getParentFile().getAbsolutePath() + File.separator + newName;
		FileRef newFileRef = new FileRef(newAbsolutePath, file.isDirectory(), 
				file.isHidden(), file.getLastModified(), file.getLength());
		fileToRename.renameTo(new File(newAbsolutePath));
		if(listeners.size() > 0) {
			fireContentChanged(
					new FileSystemEvent(this, FileSystemEvent.ACTION_RENAME, 
							FileSystemEvent.PROCESS_END, new FileRef[] {file}, newFileRef));
		}
		return newFileRef;
	}

	/**
	 * move a single file
	 * @param fromFile  the file to move
	 * @param toFile  the file to move to
	 * @throws Exception
	 */
	private boolean move(FileRef fromFile, FileRef toFile) throws Exception {
		File to = new File(toFile.getAbsolutePath());
		to.getParentFile().mkdirs();
		File sourceFile = new File(fromFile.getAbsolutePath());
		File destFile = new File (toFile.getAbsolutePath());
		sourceFile.renameTo(destFile);
		return true;
	}

	/**
	 * move one or more files to a given folder. Overwrites existing files with the same 
	 * name inside the target folder.
	 * 
	 * @param filesToMove  the the files to move
	 * @param toFolder the folder to move to
	 * @throws Exception
	 */
	public boolean move(FileRef[] filesToMove, FileRef toFolder) throws Exception {
		for(int i = 0; i < filesToMove.length; i++) {
			if(filesToMove[i].isDirectory()) {
				move(list(filesToMove[i]), mix(filesToMove[i], toFolder)); // recursion into folder
				FileRef[] filesLeft = list(filesToMove[i]);
				if(filesLeft == null || filesLeft.length == 0) {
					new File(filesToMove[i].getAbsolutePath()).delete(); // only delete folder when empty
				}
			} else
				move(filesToMove[i], mix(filesToMove[i], toFolder));
		}
		return true;
	}

	/**
	 * delete a single file
	 * @param fileToDelete  the file to delete
	 * @throws Exception
	 */
	private boolean delete(FileRef fileToDelete) throws Exception {
		File delFile = new File(fileToDelete.getAbsolutePath());
		if(delFile.exists()) {
			delFile.delete();
			if(listeners.size() > 0) {
				fireContentChanged(
						new FileSystemEvent(this, FileSystemEvent.ACTION_DELETE, 
								FileSystemEvent.PROCESS_END, new FileRef[] {fileToDelete}));
			}
		}
		return true;
	}

	/**
	 * delete one or more files
	 * @param filesToDelete  the list of files to delete
	 * @throws Exception
	 */
	public boolean delete(FileRef[] filesToDelete) throws Exception {
		for(int i = 0; i < filesToDelete.length; i++) {
			if(filesToDelete[i].isDirectory()) {
				delete(list(filesToDelete[i])); // recursion into directory
				FileRef[] files = list(filesToDelete[i]);
				if(files == null || files.length == 0) // only delete directory when empty
					delete(filesToDelete[i]);
			} else 
				delete(filesToDelete[i]);
		}
		return true;
	}

	/**
	 * extract a given ZIP archive to a given folder
	 * @param archive  the archive to extract
	 * @param toFolder  the folder to extract the contents of the archive to
	 * @throws Exception
	 */
	public boolean extract(FileRef archive, FileRef toFolder) throws Exception {
		ZipFile zipfile = new ZipFile(new File(archive.getAbsolutePath()));
		Enumeration en = zipfile.entries();
		while(en.hasMoreElements()) {
			ZipEntry zipentry = (ZipEntry) en.nextElement();
			unzip(zipfile, zipentry, toFolder.getAbsolutePath());
		}
		zipfile.close();
		return true;
	}

	/**
	 * unzip a given entry of a given zip file to a given location
	 * @param zipfile  the zip file to read an entry from
	 * @param zipentry  the zip entry to read
	 * @param destPath  the path to the destination location for the extracted content
	 * @throws IOException
	 */
	private void unzip(ZipFile zipfile, ZipEntry zipentry, String destPath) throws IOException {
		byte buf[] = new byte[1024];
		InputStream is = zipfile.getInputStream(zipentry);
		String outFileName = destPath + File.separator + zipentry.getName();
		File file = new File(outFileName);
		if(!zipentry.isDirectory()) {
			file.getParentFile().mkdirs();
			if(!file.exists())
				file.createNewFile();
			FileOutputStream fos = new FileOutputStream(file);
			int i = is.read(buf, 0, 1024);
			while(i > -1) {
				fos.write(buf, 0, i);
				i = is.read(buf, 0, 1024);
			}
			fos.close();
			is.close();
		} else {
			file.mkdirs();
		}
	}

	/**
	 * pack the contents of a given folder into a new ZIP compressed archive
	 * @param folder  the folder to pack
	 * @param archive  the archive to create from the given files
	 * @throws Exception
	 */
	public boolean pack(FileRef folder, FileRef archive) throws Exception {
		File file = new File(archive.getAbsolutePath());
		FileOutputStream fos = new FileOutputStream(file);
		CheckedOutputStream checksum = new CheckedOutputStream(fos, new Adler32());
		ZipOutputStream zos = new ZipOutputStream(checksum);
		pack(zos, folder.getAbsolutePath(), "");
		zos.flush();
		zos.finish();
		zos.close();
		fos.flush();
		fos.close();
		return true;
	}

	/**
	 * go through the given file structure recursively
	 * @param zipFile  the ZIP file to write to
	 * @param srcDir  the directory to pack during this cycle
	 * @param subDir  the subdirectory to append to names of file entries inside the archive
	 * @throws IOException
	 */
	private void pack(ZipOutputStream zipFile, String srcDir, String subDir) throws IOException {
		File[] files = new File(srcDir).listFiles();
		for(int i = 0; i < files.length; i++) {
			if(files[i].isDirectory()) {
				pack(zipFile, files[i].getAbsolutePath(), subDir + File.separator + files[i].getName());
			}
			else {
				packFile(zipFile, subDir, files[i]);
			}
		}
	}

	/**
	 * pack a given file
	 * @param zipFile  the ZIP archive to pack to
	 * @param dir  the directory name to append to name of file entry inside archive
	 * @param file  the file to pack
	 * @throws IOException
	 */
	private void packFile(ZipOutputStream zipFile, String dir, File file) throws IOException
	{
		FileInputStream fileinputstream = new FileInputStream(file);
		byte buf[] = new byte[fileinputstream.available()];
		fileinputstream.read(buf);
		String dirWithSlashes = dir.replace('\\', '/');
		//System.out.println("zipping " + dirWithSlashes + "/" + file.getName());
		ZipEntry ze = new ZipEntry(dirWithSlashes + "/" + file.getName());
		ze.setMethod(ZipEntry.DEFLATED);
		zipFile.putNextEntry(ze);
		zipFile.write(buf, 0, buf.length);
		zipFile.closeEntry();
		fileinputstream.close();
	}

	/**
	 * create a new folder
	 * 
	 * @param folder the folder to create
	 * @throws Exception
	 */
	public boolean createFolder(FileRef folder) throws Exception {
		File createFolder = new File(folder.getAbsolutePath());
		if(!createFolder.exists())
		{
			createFolder.mkdirs();
			createFolder.mkdir();
			if(listeners.size() > 0) {
				fireContentChanged(
						new FileSystemEvent(this, FileSystemEvent.ACTION_CREATE, 
								FileSystemEvent.PROCESS_END, new FileRef[] {folder}));
			}
		}
		return true;
	}

	/**
	 * determine whether or not a given file exists in this instance of file system
	 * @param file  the file to determine existence for
	 * @return  true if the given files exists in this instance of file system, false if not
	 * @throws Exception
	 */
	public boolean fileExists(FileRef file) throws Exception {
		return new File(file.getAbsolutePath()).exists();
	}

	/**
	 * create an instance of class <code>FileRef</code> from a given instance of 
	 * class <code>File</code>
	 * @param localFile  the instance of class <code>File</code> to create an instance 
	 * of class <code>FileRef</code> for
	 * @return an instance of class <code>FileRef</code> that represents the given 
	 * instance of class <code>File</code>
	 */
	private FileRef localToGenericFile(File localFile) {
		FileRef newFile = null;
		if(localFile != null) {
			newFile = new FileRef(localFile.getAbsolutePath(), localFile.isDirectory(), 
					localFile.isHidden(), localFile.lastModified(), localFile.length());
		}
		return newFile;
	}

	/**
	 * create a new FileRef my mixing a directory expression with an existing FileRef
	 * @param mergeFile  the file that has the name to mix with a given directory
	 * @param toDirectory  the directory to use to build a new absolute file name with
	 * @return  the new file
	 */
	private FileRef mix(FileRef mergeFile, FileRef toDirectory) {
		String sep = getFileSeparator();
		return new FileRef(toDirectory.getAbsolutePath() + sep + mergeFile.getName(sep));
	}

	/**
	 * determine the size of a folder in bytes
	 * @param folder  the folder to determine the size for
	 * @return  the size of the folder in bytes
	 * @throws Exception
	 */
	public double getFolderSize(FileRef folder) throws Exception {
		double size = 0;
		FileRef[] files = list(folder);
		for(int i=0; i < files.length; i++) {
			if(files[i].isDirectory()) {
				size += getFolderSize(files[i]);
			} else {
				size += files[i].getLength();
			}
		}
		return size;
	}

	/**
	 * get the url string of the machine this file system is located
	 * @return  url string of machine this file system is located
	 * @throws Exception
	 */
	public String getUrl() throws Exception {
		return FileSystem.LOCAL_FILE_SYSTEM;
	}

	/**
	 * read contents of a file to a byte array in memory
	 * @param file  reference to the file to read
	 * @return  the file contents
	 * @throws Exception
	 */
	public byte[] read(FileRef file) throws Exception {
		FileInputStream fis = new FileInputStream(file.getAbsolutePath());
		byte[] contents = new byte[fis.available()];
		fis.read(contents);
		fis.close();
		return contents;
	}

	/**
	 * write contents of a file from a byte array in memory
	 * @param file  reference to the file to write
	 * @param contents  the contents to write to the referenced file
	 * @throws Exception
	 */
	public boolean write(FileRef file, byte[] contents) throws Exception {
		FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
		fos.write(contents);
		fos.close();
		return true;
	}

	/**
	 * import from a given file system to this file system
	 * @param sourceFiles  references to the files in the source file system that are to be imported
	 * @param importFolder  folder of this file system to import to
	 * @param sourceFs  the file system to import from
	 * @throws Throwable
	 */
	public void importFiles(FileRef[] filesToCopy, FileRef toFolder, FileSystem localFs) 
	throws Throwable 
	{
		throw new Exception("importFiles is not implemented");
	}

	/**
	 * export from this file system to a given target file system
	 * @param exportFiles  references to the files in this file system to export
	 * @param targetFolder  the folder of the target files system to export to
	 * @param targetFs  the file system to export to
	 * @throws Throwable
	 */
	public void exportFiles(FileRef[] filesToCopy, FileRef toFolder, FileSystem targetFs) 
	throws Throwable 
	{
		throw new Exception("exportFiles is not implemented");
	}

	/**
	 * add a <code>FileSystemListener</code> to this file system
	 * @param listener  the listener to add
	 */
	public void addFileSystemListener(FileSystemListener listener) throws Throwable {
		listeners.add(listener);
	}

	/**
	 * remove a <code>FileSystemListener</code> from this file system
	 * @param listener  the listener to remove
	 */
	public void removeFileSystemListener(FileSystemListener listener) throws Throwable {
		listeners.remove(listener);
	}

	/**
	 * notify listeners that the content of this file system has changed
	 * @param event  the event that should accompany this notification
	 */
	protected void fireContentChanged(FileSystemEvent event) {
		Enumeration e = listeners.elements();
		while(e.hasMoreElements()) {
			Object listener = e.nextElement();
			if(listener != null && listener instanceof FileSystemListener)
				((FileSystemListener) listener).contentChanged(event);
		}
	}
}
